Замена Heap на преалоцированный Vec + Lifetime переменные
TL;DR: Такой рефакторинг привел всех в состояние Х. С 7мкс до 200нс (буст в 35 раз). В вопросе скорости создания контекстов мы выебали Kx Systems, Erlang и Python. На этом можно поставить пока точку.
Lang avg ns of 1M calls
------- ------------------
Rust 0
Java 3
O-CPS 217
Erlang 10699/1806/436/9
Python 537
K 899
LuaJIT 33856
Мы поменяли в нашем интерпретаторе все Box Heap аллокации на преалоцированный ручной хип с вектором указателей + лайфтайм по всему коду. Т.е. даже Box остались исключительно как бинды. Получился Zero-Copy Interpreter работающий исключительно с указателями на ячейки байт-кода виртуальной машины, которые в сущности сериализированные элементы AST дерева. Опуская детали реализации приведу только типы деревьев:
Старое дерево с Heap указателями
pub enum Cont {
Expressions(AST, Tape),
Lambda(Code, AST, AST, Tape),
Assign(AST, Tape),
Cond(AST, AST, Tape),
Func(AST, AST, Tape),
Call(AST, Tape),
Verb(Verb, AST, u8, Tape),
Adverb(Adverb, AST, Tape),
Return,
}
pub struct Tape {
env: Rc>,
cont: Box,
}
Новое дерево только с абстрактными ссылками и переменными времени жизни
pub enum Cont<'a> {
Expressions(&'a AST<'ast>, &'a Cont<'a>),
Lambda(Code, &'a AST<'a>, &'a AST<'a>, &'a Cont<'a>),
Assign(&'a AST<'ast>, &'ast Cont<'ast>),
Cond(&'a AST<'a>, &'a AST<'a>, &'a Cont<'a>),
Func(&'a AST<'a>, &'a AST<'a>, &'a Cont<'a>),
Call(&'a AST<'a>, &'a Cont<'a>),
Verb(Verb, &'a AST<'a>, u8, &'a Cont<'a>),
Adverb(Adverb, &'a AST<'a>, &'a Cont<'a>),
Return,
}
pub struct Interpreter<'a> {
arena: Arena<'a>,
env: Environment<'a>,
}
Environment в наивном прототипе представлен в виде HashMap, который как известно неподходит там где нужна скорость, зато понятно — у каждой кложи своя хеш-таблица контекст. Другое дело что для этого всего хватит и стека фреймов. Вообще говоря контексты функций — это и есть стек, а перенесен он в Heap для того, чтобы можно было писать не валящиеся бесконечные циклы, что называется tailcall или CPS интерпретаторами.
Написать прототип на расте оказалось очень просто. Бездумный ленивый CPS интерпретатор, который в хипе держит свой стек в виде HashMap изначально является не хуже чем LuaJIT в вопросе создания контекстов, а переписанный нормально в zero-copy версию с лайфтаймами с стек-фреймами в преалоцированном векторе обгоняет даже лидеров индустрии.
В рабочем режиме расставить амперсанты по коду заняло приблизительно 3 дня (чтобы получить 200нс версию), столько же времени ушло на создание изначального 7мкс прототипа. За скобками осталось влияние замены хештаблиц в качестве носителей контекстов на стековые фреймы в носителе-векторе, думаю заслуга этой части тоже немалая.
Это не первый интерпретатор с монотонно-растущими адресами, а значит существует функция ограничивающая расстояние до самого древнего индекса в ленте интерпретатора, на который хоть кто-то ссылается. Хочется сделать так чтобы расстояние до этого самого древнего индекса всегда было меньше N, чтобы закольцевать и замуровать его в ограниченном буфере-очереди-стриме. Сам интерпретатор является продюсером и консамером четырех стримов, т.е. его устройство полностью линеаризировано.